/******************************************************************************* * Copyright (c) 2005, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Patrik Suzzi <psuzzi@gmail.com> - Bug 490700 *******************************************************************************/ package org.eclipse.ui.internal.operations; import java.lang.reflect.InvocationTargetException; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.operations.IAdvancedUndoableOperation; import org.eclipse.core.commands.operations.IAdvancedUndoableOperation2; import org.eclipse.core.commands.operations.IOperationApprover; import org.eclipse.core.commands.operations.IOperationApprover2; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.IUndoableOperation; import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.window.Window; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.WorkbenchMessages; import org.eclipse.ui.internal.WorkbenchPlugin; import org.eclipse.ui.internal.misc.StatusUtil; /** * <p> * An operation approver that rechecks the validity of a proposed undo or redo * operation using * {@link IAdvancedUndoableOperation#computeUndoableStatus(IProgressMonitor)} or * {@link IAdvancedUndoableOperation#computeRedoableStatus(IProgressMonitor)}. * Some complex operations do not compute their validity in canUndo() or * canRedo() because it is too time-consuming. To save time on complex * validations, the true validity is not determined until it is time to perform * the operation. * </p> * <p> * Since 3.3, this operation approver also checks the validity of a proposed * execute by determining whether the redo is viable. * * @since 3.1 */ public class AdvancedValidationUserApprover implements IOperationApprover, IOperationApprover2 { /** * Static to prevent opening of error dialogs for automated testing. * * @since 3.3 */ public static boolean AUTOMATED_MODE = false; private IUndoContext context; private static final int EXECUTING = 1; private static final int UNDOING = 2; private static final int REDOING = 3; private class StatusReportingRunnable implements IRunnableWithProgress { IStatus status; int doing; IUndoableOperation operation; IAdaptable uiInfo; StatusReportingRunnable(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo, int doing) { super(); this.operation = operation; this.doing = doing; this.uiInfo = uiInfo; } // The casts to IAdvancedUndoableOperation and // IAdvancedUndoableOperation2 are safe because these types were checked // in the call chain. @Override public void run(IProgressMonitor pm) { try { switch (doing) { case UNDOING: status = ((IAdvancedUndoableOperation) operation) .computeUndoableStatus(pm); break; case REDOING: status = ((IAdvancedUndoableOperation) operation) .computeRedoableStatus(pm); break; case EXECUTING: status = ((IAdvancedUndoableOperation2) operation) .computeExecutionStatus(pm); break; } } catch (ExecutionException e) { reportException(e, uiInfo); status = IOperationHistory.OPERATION_INVALID_STATUS; } } IStatus getStatus() { return status; } } /** * Create an AdvancedValidationUserApprover that performs advanced * validations on proposed undo and redo operations for a given undo * context. * * @param context - * the undo context of operations in question. */ public AdvancedValidationUserApprover(IUndoContext context) { super(); this.context = context; } @Override public IStatus proceedRedoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) { return proceedWithOperation(operation, history, uiInfo, REDOING); } @Override public IStatus proceedUndoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) { return proceedWithOperation(operation, history, uiInfo, UNDOING); } @Override public IStatus proceedExecuting(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) { return proceedWithOperation(operation, history, uiInfo, EXECUTING); } /* * Determine whether the operation in question is still valid. */ private IStatus proceedWithOperation(final IUndoableOperation operation, final IOperationHistory history, final IAdaptable uiInfo, final int doing) { // return immediately if the operation is not relevant if (!operation.hasContext(context)) { return Status.OK_STATUS; } // if the operation does not support advanced validation, // then we assume it is valid. if (doing == EXECUTING) { if (!(operation instanceof IAdvancedUndoableOperation2)) { return Status.OK_STATUS; } } else { if (!(operation instanceof IAdvancedUndoableOperation)) { return Status.OK_STATUS; } } // The next two methods make a number of UI calls, so we wrap the // whole thing up in a syncExec. final IStatus[] status = new IStatus[1]; PlatformUI.getWorkbench().getDisplay().syncExec(() -> { // Compute the undoable or redoable status status[0] = computeOperationStatus(operation, history, uiInfo, doing); // Report non-OK statuses to the user. In some cases, the user // may choose to proceed, and the returned status will be // different than what is reported. if (!status[0].isOK()) { status[0] = reportAndInterpretStatus(status[0], uiInfo, operation, doing); } }); // If the operation is still not OK, inform the history that the // operation has changed, since it was previously believed to be valid. // We rely here on the ability of an IAdvancedUndoableOperation to // correctly report canUndo() and canRedo() once the undoable and // redoable status have been computed. if (!status[0].isOK()) { history.operationChanged(operation); } return status[0]; } private IStatus computeOperationStatus(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo, int doing) { try { StatusReportingRunnable runnable = new StatusReportingRunnable( operation, history, uiInfo, doing); TimeTriggeredProgressMonitorDialog progressDialog = new TimeTriggeredProgressMonitorDialog( getShell(uiInfo), PlatformUI.getWorkbench() .getProgressService().getLongOperationTime()); progressDialog.run(false, true, runnable); return runnable.getStatus(); } catch (OperationCanceledException e) { return Status.CANCEL_STATUS; } catch (InvocationTargetException e) { reportException(e, uiInfo); return IOperationHistory.OPERATION_INVALID_STATUS; } catch (InterruptedException e) { // Operation was cancelled and acknowledged by runnable with this // exception. Do nothing. return Status.CANCEL_STATUS; } } /* * Report the specified execution exception to the log and to the user. */ private void reportException(Exception e, IAdaptable uiInfo) { Throwable nestedException = StatusUtil.getCause(e); Throwable exception = (nestedException == null) ? e : nestedException; String title = WorkbenchMessages.Error; String message = WorkbenchMessages.WorkbenchWindow_exceptionMessage; String exceptionMessage = exception.getMessage(); if (exceptionMessage == null) { exceptionMessage = message; } IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, exceptionMessage, exception); WorkbenchPlugin.log(message, status); boolean createdShell = false; Shell shell = getShell(uiInfo); if (shell == null) { createdShell = true; shell = new Shell(); } ErrorDialog.openError(shell, title, message, status); if (createdShell) { shell.dispose(); } } /* * Report a non-OK status to the user */ private IStatus reportAndInterpretStatus(IStatus status, IAdaptable uiInfo, IUndoableOperation operation, int doing) { // Nothing to report if we are running automated tests. We will treat // warnings as if they were authorized by the user. if (AUTOMATED_MODE) { if (status.getSeverity() == IStatus.WARNING) { return Status.OK_STATUS; } return status; } // CANCEL status is assumed to be initiated by the user, so there // is nothing to report. if (status.getSeverity() == IStatus.CANCEL) { return status; } // Other status severities are reported with a message dialog. // First obtain a shell and set up the dialog title. boolean createdShell = false; IStatus reportedStatus = status; Shell shell = getShell(uiInfo); if (shell == null) { createdShell = true; shell = new Shell(); } // Set up the dialog. For non-error statuses, we use a warning dialog // that allows the user to proceed or to cancel out of the operation. if (!(status.getSeverity() == IStatus.ERROR)) { String warning, title; switch (doing) { case UNDOING: warning = WorkbenchMessages.Operations_proceedWithNonOKUndoStatus; if (status.getSeverity() == IStatus.INFO) { title = WorkbenchMessages.Operations_undoInfo; } else { title = WorkbenchMessages.Operations_undoWarning; } break; case REDOING: warning = WorkbenchMessages.Operations_proceedWithNonOKRedoStatus; if (status.getSeverity() == IStatus.INFO) { title = WorkbenchMessages.Operations_redoInfo; } else { title = WorkbenchMessages.Operations_redoWarning; } break; default: // EXECUTING warning = WorkbenchMessages.Operations_proceedWithNonOKExecuteStatus; if (status.getSeverity() == IStatus.INFO) { title = WorkbenchMessages.Operations_executeInfo; } else { title = WorkbenchMessages.Operations_executeWarning; } break; } String message = NLS.bind(warning, new Object[] { status.getMessage(), operation.getLabel() }); String[] buttons = new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }; MessageDialog dialog = new MessageDialog(shell, title, null, message, MessageDialog.WARNING, 0, buttons); int dialogAnswer = dialog.open(); // The user has been given the specific status and has chosen // to proceed or to cancel. The user choice determines what // the status should be at this point, OK or CANCEL. if (dialogAnswer == Window.OK) { reportedStatus = Status.OK_STATUS; } else { reportedStatus = Status.CANCEL_STATUS; } } else { String title, stopped; switch (doing) { case UNDOING: title = WorkbenchMessages.Operations_undoProblem; stopped = WorkbenchMessages.Operations_stoppedOnUndoErrorStatus; break; case REDOING: title = WorkbenchMessages.Operations_redoProblem; stopped = WorkbenchMessages.Operations_stoppedOnRedoErrorStatus; break; default: // EXECUTING title = WorkbenchMessages.Operations_executeProblem; stopped = WorkbenchMessages.Operations_stoppedOnExecuteErrorStatus; break; } // It is an error condition. The user has no choice to proceed, so // we only report what has gone on. We use a warning icon instead of // an error icon since there has not yet been a failure. String message = NLS.bind(stopped, status.getMessage(), operation .getLabel()); MessageDialog dialog = new MessageDialog(shell, title, null, message, MessageDialog.WARNING, new String[] { IDialogConstants.OK_LABEL }, 0); // ok dialog.open(); } if (createdShell) { shell.dispose(); } return reportedStatus; } /* * Return the shell described by the supplied uiInfo, or null if no shell is * described. */ Shell getShell(IAdaptable uiInfo) { if (uiInfo != null) { Shell shell = Adapters.adapt(uiInfo, Shell.class); if (shell != null) { return shell; } } return null; } }